1
0
mirror of https://github.com/django/django.git synced 2025-07-04 01:39:20 +00:00

gis: Refactor of the GeoQuerySet; new features include:

(1) Creation of internal API that eases generation of `GeoQuerySet` methods.
 (2) `GeoQuerySet.distance` now returns `Distance` objects instead of floats.
 (3) Added the new `GeoQuerySet` methods: `area`, `centroid`, `difference`, `envelope`, `intersection`, `length`, `make_line`, `mem_size`, `num_geom`, `num_points`, `perimeter`, `point_on_surface`, `scale`, `svg`, `sym_difference`, `translate`, `union`.
 (4) The `model_att` keyword may be used to customize the attribute that `GeoQuerySet` methods attach output to.
 (5) Geographic distance lookups and `GeoQuerySet.distance` calls now use `ST_distance_sphere` by default (performance benefits far outweigh small loss in accuracy); `ST_distance_spheroid` may still be used by specifying an option.
 (6) `GeoQuerySet` methods may now operate accross ForeignKey relations specified via the `field_name` keyword (but this does not work on Oracle).
 (7) `Area` now has the same units of measure as `Distance`.

Backward Incompatibilites:
 * The aggregate union method is now known as `unionagg`.
 * The `field_name` keyword used for `GeoQuerySet` methods may no longer be specified via positional arguments.
 * `Distance` objects returned instead of floats from `GeoQuerySet.distance`.
 * `ST_Distance_sphere` used by default for geographic distance calculations.


git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7641 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2008-06-15 19:48:57 +00:00
parent eb30cad66a
commit 4ec80c4333
27 changed files with 1369 additions and 546 deletions

View File

@ -3,97 +3,16 @@
Specifically, this module will import the correct routines and modules
needed for GeoDjango to interface with the spatial database.
Some of the more important classes and routines from the spatial backend
include:
(1) `GeoBackEndField`, a base class needed for GeometryField.
(2) `get_geo_where_clause`, a routine used by `GeoWhereNode`.
(3) `GIS_TERMS`, a listing of all valid GeoDjango lookup types.
(4) `SpatialBackend`, a container object for information specific to the
spatial backend.
"""
from django.conf import settings
from django.db.models.sql.query import QUERY_TERMS
from django.contrib.gis.db.backend.util import gqn
# These routines (needed by GeoManager), default to False.
ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8))
# Lookup types in which the rest of the parameters are not
# needed to be substitute in the WHERE SQL (e.g., the 'relate'
# operation on Oracle does not need the mask substituted back
# into the query SQL.).
LIMITED_WHERE = []
# Retrieving the necessary settings from the backend.
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis.adaptor import \
PostGISAdaptor as GeoAdaptor
from django.contrib.gis.db.backend.postgis.field import \
PostGISField as GeoBackendField
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
from django.contrib.gis.db.backend.postgis.query import \
get_geo_where_clause, POSTGIS_TERMS as GIS_TERMS, \
ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, DISTANCE_FUNCTIONS, \
EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
# PostGIS version info is needed to determine calling order of some
# stored procedures (e.g., AsGML()).
VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
SPATIAL_BACKEND = 'postgis'
from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
from django.contrib.gis.db.backend.oracle.field import \
OracleSpatialField as GeoBackendField
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
from django.contrib.gis.db.backend.oracle.query import \
get_geo_where_clause, ORACLE_SPATIAL_TERMS as GIS_TERMS, \
ASGML, DISTANCE, DISTANCE_FUNCTIONS, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'oracle'
LIMITED_WHERE = ['relate']
from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'mysql':
from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
from django.contrib.gis.db.backend.mysql.field import \
MySQLGeoField as GeoBackendField
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
from django.contrib.gis.db.backend.mysql.query import \
get_geo_where_clause, MYSQL_GIS_TERMS as GIS_TERMS, GEOM_SELECT
DISTANCE_FUNCTIONS = {}
SPATIAL_BACKEND = 'mysql'
from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
class SpatialBackend(object):
"A container for properties of the SpatialBackend."
# Stored procedure names used by the `GeoManager`.
as_kml = ASKML
as_gml = ASGML
distance = DISTANCE
distance_spheroid = DISTANCE_SPHEROID
extent = EXTENT
name = SPATIAL_BACKEND
select = GEOM_SELECT
transform = TRANSFORM
union = UNION
# Version information, if defined.
version = VERSION
# All valid GIS lookup terms, and distance functions.
gis_terms = GIS_TERMS
distance_functions = DISTANCE_FUNCTIONS
# Lookup types where additional WHERE parameters are excluded.
limited_where = LIMITED_WHERE
# Shortcut booleans.
mysql = SPATIAL_BACKEND == 'mysql'
oracle = SPATIAL_BACKEND == 'oracle'
postgis = SPATIAL_BACKEND == 'postgis'
# Class for the backend field.
Field = GeoBackendField
# Adaptor class used for quoting GEOS geometries in the database.
Adaptor = GeoAdaptor

View File

@ -0,0 +1,29 @@
"""
This module holds the base `SpatialBackend` object, which is
instantiated by each spatial backend with the features it has.
"""
# TODO: Create a `Geometry` protocol and allow user to use
# different Geometry objects -- for now we just use GEOSGeometry.
from django.contrib.gis.geos import GEOSGeometry, GEOSException
class BaseSpatialBackend(object):
Geometry = GEOSGeometry
GeometryException = GEOSException
def __init__(self, **kwargs):
kwargs.setdefault('distance_functions', {})
kwargs.setdefault('limited_where', {})
for k, v in kwargs.iteritems(): setattr(self, k, v)
def __getattr__(self, name):
"""
All attributes of the spatial backend return False by default.
"""
try:
return self.__dict__[name]
except KeyError:
return False

View File

@ -1 +1,13 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField
from django.contrib.gis.db.backend.mysql.query import *
SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True,
gis_terms=MYSQL_GIS_TERMS,
select=GEOM_SELECT,
Adaptor=WKTAdaptor,
Field=MySQLGeoField)

View File

@ -0,0 +1,31 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
from django.contrib.gis.db.backend.oracle.query import *
SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
area=AREA,
centroid=CENTROID,
difference=DIFFERENCE,
distance=DISTANCE,
distance_functions=DISTANCE_FUNCTIONS,
gis_terms=ORACLE_SPATIAL_TERMS,
gml=ASGML,
intersection=INTERSECTION,
length=LENGTH,
limited_where = {'relate' : None},
num_geom=NUM_GEOM,
num_points=NUM_POINTS,
perimeter=LENGTH,
point_on_surface=POINT_ON_SURFACE,
select=GEOM_SELECT,
sym_difference=SYM_DIFFERENCE,
transform=TRANSFORM,
unionagg=UNIONAGG,
union=UNION,
Adaptor=OracleSpatialAdaptor,
Field=OracleSpatialField,
)

View File

@ -0,0 +1,5 @@
from cx_Oracle import CLOB
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
class OracleSpatialAdaptor(WKTAdaptor):
input_size = CLOB

View File

@ -15,10 +15,21 @@ from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name
# The GML, distance, transform, and union procedures.
AREA = 'SDO_GEOM.SDO_AREA'
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
CENTROID = 'SDO_GEOM.SDO_CENTROID'
DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE'
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
EXTENT = 'SDO_AGGR_MBR'
INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION'
LENGTH = 'SDO_GEOM.SDO_LENGTH'
NUM_GEOM = 'SDO_UTIL.GETNUMELEM'
NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES'
POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE'
SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR'
TRANSFORM = 'SDO_CS.TRANSFORM'
UNION = 'SDO_AGGR_UNION'
UNION = 'SDO_GEOM.SDO_UNION'
UNIONAGG = 'SDO_AGGR_UNION'
# We want to get SDO Geometries as WKT because it is much easier to
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.

View File

@ -0,0 +1,42 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
from django.contrib.gis.db.backend.postgis.field import PostGISField
from django.contrib.gis.db.backend.postgis.query import *
SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
area=AREA,
centroid=CENTROID,
difference=DIFFERENCE,
distance=DISTANCE,
distance_functions=DISTANCE_FUNCTIONS,
distance_sphere=DISTANCE_SPHERE,
distance_spheroid=DISTANCE_SPHEROID,
envelope=ENVELOPE,
extent=EXTENT,
gis_terms=POSTGIS_TERMS,
gml=ASGML,
intersection=INTERSECTION,
kml=ASKML,
length=LENGTH,
length_spheroid=LENGTH_SPHEROID,
make_line=MAKE_LINE,
mem_size=MEM_SIZE,
num_geom=NUM_GEOM,
num_points=NUM_POINTS,
perimeter=PERIMETER,
point_on_surface=POINT_ON_SURFACE,
scale=SCALE,
select=GEOM_SELECT,
svg=ASSVG,
sym_difference=SYM_DIFFERENCE,
transform=TRANSFORM,
translate=TRANSLATE,
union=UNION,
unionagg=UNIONAGG,
version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
Adaptor=PostGISAdaptor,
Field=PostGISField,
)

View File

@ -1,6 +1,5 @@
from django.db import connection
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.backend.util import gqn
from django.contrib.gis.db.backend.postgis.query import TRANSFORM

View File

@ -21,7 +21,7 @@ if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
# means that 'ST_' is prefixes geometry function names.
# means that 'ST_' prefixes geometry function names.
GEOM_FUNC_PREFIX = ''
if MAJOR_VERSION >= 1:
if (MINOR_VERSION1 > 2 or
@ -30,26 +30,46 @@ if MAJOR_VERSION >= 1:
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
# Custom selection not needed for PostGIS since GEOS geometries may be
# Custom selection not needed for PostGIS because GEOS geometries are
# 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)'
# e.g,, 'AsText(%s)'.
GEOM_SELECT = None
# Functions used by the GeoManager & GeoQuerySet
AREA = get_func('Area')
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
ASSVG = get_func('AsSVG')
CENTROID = get_func('Centroid')
DIFFERENCE = get_func('Difference')
DISTANCE = get_func('Distance')
DISTANCE_SPHERE = get_func('distance_sphere')
DISTANCE_SPHEROID = get_func('distance_spheroid')
ENVELOPE = get_func('Envelope')
EXTENT = get_func('extent')
GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB')
INTERSECTION = get_func('Intersection')
LENGTH = get_func('Length')
LENGTH_SPHEROID = get_func('length_spheroid')
MAKE_LINE = get_func('MakeLine')
MEM_SIZE = get_func('mem_size')
NUM_GEOM = get_func('NumGeometries')
NUM_POINTS = get_func('npoints')
PERIMETER = get_func('Perimeter')
POINT_ON_SURFACE = get_func('PointOnSurface')
SCALE = get_func('Scale')
SYM_DIFFERENCE = get_func('SymDifference')
TRANSFORM = get_func('Transform')
TRANSLATE = get_func('Translate')
# Special cases for union and KML methods.
if MINOR_VERSION1 < 3:
UNION = 'GeomUnion'
UNIONAGG = 'GeomUnion'
UNION = 'Union'
else:
UNIONAGG = 'ST_Union'
UNION = 'ST_Union'
if MINOR_VERSION1 == 1:
@ -80,14 +100,21 @@ class PostGISDistance(PostGISFunction):
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISSphereDistance(PostGISFunction):
"For PostGIS spherical distance operations."
class PostGISSpheroidDistance(PostGISFunction):
"For PostGIS spherical distance operations (using the spheroid)."
dist_func = 'distance_spheroid'
def __init__(self, operator):
# An extra parameter in `end_subst` is needed for the spheroid string.
super(PostGISSphereDistance, self).__init__(self.dist_func,
beg_subst='%s(%s, %%s, %%s',
end_subst=') %s %s',
super(PostGISSpheroidDistance, self).__init__(self.dist_func,
beg_subst='%s(%s, %%s, %%s',
end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISSphereDistance(PostGISFunction):
"For PostGIS spherical distance operations."
dist_func = 'distance_sphere'
def __init__(self, operator):
super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s',
operator=operator, result='%%s')
class PostGISRelate(PostGISFunctionParam):
@ -164,7 +191,7 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
dtypes = (Decimal, Distance, float, int, long)
def get_dist_ops(operator):
"Returns operations for both regular and spherical distances."
return (PostGISDistance(operator), PostGISSphereDistance(operator))
return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
DISTANCE_FUNCTIONS = {
'distance_gt' : (get_dist_ops('>'), dtypes),
'distance_gte' : (get_dist_ops('>='), dtypes),
@ -193,6 +220,13 @@ 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 = tuple(POSTGIS_TERMS) # Making immutable
# For checking tuple parameters -- not very pretty but gets job done.
def exactly_two(val): return val == 2
def two_to_three(val): return val >= 2 and val <=3
def num_params(lookup_type, val):
if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
else: return exactly_two(val)
#### The `get_geo_where_clause` function for PostGIS. ####
def get_geo_where_clause(lookup_type, table_prefix, field, value):
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
@ -216,8 +250,10 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, (tuple, list)):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
if len(value) != 2:
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
# Number of valid tuple parameters depends on the lookup type.
nparams = len(value)
if not num_params(lookup_type, nparams):
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
@ -234,7 +270,9 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
if value[0].geom_typeid != 0:
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
op = op[1]
# Setting up the geodetic operation appropriately.
if nparams == 3 and value[2] == 'spheroid': op = op[2]
else: op = op[1]
else:
op = op[0]
else:

View File

@ -3,15 +3,12 @@ from django.db import connection
from django.contrib.gis.db.backend import SpatialBackend, gqn
# GeometryProxy, GEOS, Distance, and oldforms imports.
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.measure import Distance
from django.contrib.gis.oldforms import WKTField
# Attempting to get the spatial reference system.
try:
from django.contrib.gis.models import SpatialRefSys
except ImportError:
SpatialRefSys = None
# The `get_srid_info` function gets SRID information from the spatial
# reference system table w/o using the ORM.
from django.contrib.gis.models import get_srid_info
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
class GeometryField(SpatialBackend.Field):
@ -47,29 +44,7 @@ class GeometryField(SpatialBackend.Field):
# Setting the SRID and getting the units. Unit information must be
# easily available in the field instance for distance queries.
self._srid = srid
if SpatialRefSys:
# Getting the spatial reference WKT associated with the SRID from the
# `spatial_ref_sys` (or equivalent) spatial database table.
#
# The following doesn't work: SpatialRefSys.objects.get(srid=srid)
# Why? `syncdb` fails to recognize installed geographic models when there's
# an ORM query instantiated within a model field.
cur = connection.cursor()
qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
'wkt_col' : qn(SpatialRefSys.wkt_col()),
'srid_col' : qn('srid'),
'srid' : srid,
}
cur.execute(stmt)
srs_wkt = cur.fetchone()[0]
# Getting metadata associated with the spatial reference system identifier.
# Specifically, getting the unit information and spheroid information
# (both required for distance queries).
self._unit, self._unit_name = SpatialRefSys.get_units(srs_wkt)
self._spheroid = SpatialRefSys.get_spheroid(srs_wkt)
self._unit, self._unit_name, self._spheroid = get_srid_info(srid)
# Setting the dimension of the geometry field.
self._dim = dim
@ -79,19 +54,26 @@ class GeometryField(SpatialBackend.Field):
### Routines specific to GeometryField ###
@property
def geodetic(self):
"""
Returns true if this field's SRID corresponds with a coordinate
system that uses non-projected units (e.g., latitude/longitude).
"""
return self._unit_name in self.geodetic_units
def get_distance(self, dist, lookup_type):
def get_distance(self, dist_val, lookup_type):
"""
Returns a distance number in units of the field. For example, if
`D(km=1)` was passed in and the units of the field were in meters,
then 1000 would be returned.
"""
postgis = SpatialBackend.name == 'postgis'
# Getting the distance parameter and any options.
if len(dist_val) == 1: dist, option = dist_val[0], None
else: dist, option = dist_val
if isinstance(dist, Distance):
if self.geodetic:
# Won't allow Distance objects w/DWithin lookups on PostGIS.
if postgis and lookup_type == 'dwithin':
if SpatialBackend.postgis and lookup_type == 'dwithin':
raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
# Spherical distance calculation parameter should be in meters.
dist_param = dist.m
@ -101,8 +83,10 @@ class GeometryField(SpatialBackend.Field):
# Assuming the distance is in the units of the field.
dist_param = dist
# Sphereical distance query; returning meters.
if postgis and self.geodetic and lookup_type != 'dwithin':
if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid':
# On PostGIS, by default `ST_distance_sphere` is used; but if the
# accuracy of `ST_distance_spheroid` is needed than the spheroid
# needs to be passed to the SQL stored procedure.
return [gqn(self._spheroid), dist_param]
else:
return [dist_param]
@ -119,12 +103,12 @@ class GeometryField(SpatialBackend.Field):
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
if isinstance(geom, GEOSGeometry):
if isinstance(geom, SpatialBackend.Geometry):
pass
elif isinstance(geom, basestring):
try:
geom = GEOSGeometry(geom)
except GEOSException:
geom = SpatialBackend.Geometry(geom)
except SpatialBackend.GeometryException:
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
else:
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
@ -148,8 +132,8 @@ class GeometryField(SpatialBackend.Field):
def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name)
# Setup for lazy-instantiated GEOSGeometry object.
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
# Setup for lazy-instantiated Geometry object.
setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
def get_db_prep_lookup(self, lookup_type, value):
"""
@ -166,7 +150,7 @@ class GeometryField(SpatialBackend.Field):
geom = self.get_geometry(value)
# Getting the WHERE clause list and the associated params list. The params
# list is populated with the Adaptor wrapping the GEOSGeometry for the
# list is populated with the Adaptor wrapping the Geometry for the
# backend. The WHERE clause list contains the placeholder for the adaptor
# (e.g. any transformation SQL).
where = [self.get_placeholder(geom)]
@ -175,7 +159,7 @@ class GeometryField(SpatialBackend.Field):
if isinstance(value, (tuple, list)):
if lookup_type in SpatialBackend.distance_functions:
# Getting the distance parameter in the units of the field.
where += self.get_distance(value[1], lookup_type)
where += self.get_distance(value[1:], lookup_type)
elif lookup_type in SpatialBackend.limited_where:
pass
else:
@ -187,15 +171,15 @@ class GeometryField(SpatialBackend.Field):
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
if isinstance(value, GEOSGeometry):
if isinstance(value, SpatialBackend.Geometry):
return SpatialBackend.Adaptor(value)
elif value is None:
return None
else:
raise TypeError('Geometry Proxy should only return GEOSGeometry objects or None.')
raise TypeError('Geometry Proxy should only return Geometry objects or None.')
def get_manipulator_field_objs(self):
"Using the WKTField (defined above) to be our manipulator."
"Using the WKTField (oldforms) to be our manipulator."
return [WKTField]
# The OpenGIS Geometry Type Fields

View File

@ -7,20 +7,71 @@ class GeoManager(Manager):
def get_query_set(self):
return GeoQuerySet(model=self.model)
def area(self, *args, **kwargs):
return self.get_query_set().area(*args, **kwargs)
def centroid(self, *args, **kwargs):
return self.get_query_set().centroid(*args, **kwargs)
def difference(self, *args, **kwargs):
return self.get_query_set().difference(*args, **kwargs)
def distance(self, *args, **kwargs):
return self.get_query_set().distance(*args, **kwargs)
def envelope(self, *args, **kwargs):
return self.get_query_set().envelope(*args, **kwargs)
def extent(self, *args, **kwargs):
return self.get_query_set().extent(*args, **kwargs)
def gml(self, *args, **kwargs):
return self.get_query_set().gml(*args, **kwargs)
def intersection(self, *args, **kwargs):
return self.get_query_set().intersection(*args, **kwargs)
def kml(self, *args, **kwargs):
return self.get_query_set().kml(*args, **kwargs)
def length(self, *args, **kwargs):
return self.get_query_set().length(*args, **kwargs)
def make_line(self, *args, **kwargs):
return self.get_query_set().make_line(*args, **kwargs)
def mem_size(self, *args, **kwargs):
return self.get_query_set().mem_size(*args, **kwargs)
def num_geom(self, *args, **kwargs):
return self.get_query_set().num_geom(*args, **kwargs)
def num_points(self, *args, **kwargs):
return self.get_query_set().num_points(*args, **kwargs)
def perimeter(self, *args, **kwargs):
return self.get_query_set().perimeter(*args, **kwargs)
def point_on_surface(self, *args, **kwargs):
return self.get_query_set().point_on_surface(*args, **kwargs)
def scale(self, *args, **kwargs):
return self.get_query_set().scale(*args, **kwargs)
def svg(self, *args, **kwargs):
return self.get_query_set().svg(*args, **kwargs)
def sym_difference(self, *args, **kwargs):
return self.get_query_set().sym_difference(*args, **kwargs)
def transform(self, *args, **kwargs):
return self.get_query_set().transform(*args, **kwargs)
def translate(self, *args, **kwargs):
return self.get_query_set().translate(*args, **kwargs)
def union(self, *args, **kwargs):
return self.get_query_set().union(*args, **kwargs)
def unionagg(self, *args, **kwargs):
return self.get_query_set().unionagg(*args, **kwargs)

View File

@ -1,7 +1,7 @@
"""
The GeometryProxy object, allows for lazy-geometries. The proxy uses
Python descriptors for instantiating and setting GEOS Geometry objects
corresponding to geographic model fields.
Python descriptors for instantiating and setting Geometry objects
corresponding to geographic model fields.
Thanks to Robert Coup for providing this functionality (see #4322).
"""
@ -31,8 +31,8 @@ class GeometryProxy(object):
elif (geom_value is None) or (geom_value==''):
geom = None
else:
# Otherwise, a GEOSGeometry object is built using the field's contents,
# and the model's corresponding attribute is set.
# Otherwise, a Geometry object is built using the field's contents,
# and the model's corresponding attribute is set.
geom = self._klass(geom_value)
setattr(obj, self._field.attname, geom)
return geom

View File

@ -1,12 +1,12 @@
from itertools import izip
from django.core.exceptions import ImproperlyConfigured
from django.db import connection
from django.db.models.query import sql, QuerySet, Q
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models.fields import GeometryField, PointField
from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode
from django.contrib.gis.geos import GEOSGeometry, Point
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
from django.contrib.gis.measure import Area, Distance
from django.contrib.gis.models import get_srid_info
qn = connection.ops.quote_name
# For backwards-compatibility; Q object should work just fine
@ -28,174 +28,236 @@ class GeoQuerySet(QuerySet):
super(GeoQuerySet, self).__init__(model=model, query=query)
self.query = query or GeoQuery(self.model, connection)
def distance(self, *args, **kwargs):
def area(self, tolerance=0.05, **kwargs):
"""
Returns the area of the geographic field in an `area` attribute on
each element of this GeoQuerySet.
"""
# Peforming setup here rather than in `_spatial_attribute` so that
# we can get the units for `AreaField`.
procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
s = {'procedure_args' : procedure_args,
'geo_field' : geo_field,
'setup' : False,
}
if SpatialBackend.oracle:
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance
s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
elif SpatialBackend.postgis:
if not geo_field.geodetic:
# Getting the area units of the geographic field.
s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
else:
# TODO: Do we want to support raw number areas for geodetic fields?
raise Exception('Area on geodetic coordinate systems not supported.')
return self._spatial_attribute('area', s, **kwargs)
def centroid(self, **kwargs):
"""
Returns the centroid of the geographic field in a `centroid`
attribute on each element of this GeoQuerySet.
"""
return self._geom_attribute('centroid', **kwargs)
def difference(self, geom, **kwargs):
"""
Returns the spatial difference of the geographic field in a `difference`
attribute on each element of this GeoQuerySet.
"""
return self._geomset_attribute('difference', geom, **kwargs)
def distance(self, geom, **kwargs):
"""
Returns the distance from the given geographic field name to the
given geometry in a `distance` attribute on each element of the
GeoQuerySet.
Keyword Arguments:
`spheroid` => If the geometry field is geodetic and PostGIS is
the spatial database, then the more accurate
spheroid calculation will be used instead of the
quicker sphere calculation.
`tolerance` => Used only for Oracle. The tolerance is
in meters -- a default of 5 centimeters (0.05)
is used.
"""
DISTANCE = SpatialBackend.distance
if not DISTANCE:
raise ImproperlyConfigured('Distance() stored proecedure not available.')
return self._distance_attribute('distance', geom, **kwargs)
# Getting the geometry field and GEOSGeometry object to base distance
# calculations from.
nargs = len(args)
if nargs == 1:
field_name = None
geom = args[0]
elif nargs == 2:
field_name, geom = args
else:
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
def envelope(self, **kwargs):
"""
Returns a Geometry representing the bounding box of the
Geometry field in an `envelope` attribute on each element of
the GeoQuerySet.
"""
return self._geom_attribute('envelope', **kwargs)
# Getting the GeometryField and quoted column.
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('Distance output only available on GeometryFields.')
geo_col = self.query._field_column(geo_field)
# Using the field's get_db_prep_lookup() to get any needed
# transformation SQL -- we pass in a 'dummy' `contains`
# `distance_lte` lookup type.
where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
if SpatialBackend.oracle:
# The `tolerance` keyword may be used for Oracle; the tolerance is
# in meters -- a default of 5 centimeters is used.
tolerance = kwargs.get('tolerance', 0.05)
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, where[0], tolerance)}
else:
if len(where) == 3:
# Spherical distance calculation was requested (b/c spheroid
# parameter was attached) However, the PostGIS ST_distance_spheroid()
# procedure may only do queries from point columns to point geometries
# some error checking is required.
if not isinstance(geo_field, PointField):
raise TypeError('Spherical distance calculation only supported on PointFields.')
if not isinstance(GEOSGeometry(buffer(params[0].wkb)), Point):
raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
# Call to distance_spheroid() requires the spheroid as well.
dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, where[0], where[1])
else:
dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, where[0])
dist_select = {'distance' : dist_sql}
return self.extra(select=dist_select, select_params=params)
def extent(self, field_name=None):
def extent(self, **kwargs):
"""
Returns the extent (aggregate) of the features in the GeoQuerySet. The
extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
"""
EXTENT = SpatialBackend.extent
if not EXTENT:
raise ImproperlyConfigured('Extent stored procedure not available.')
convert_extent = None
if SpatialBackend.postgis:
def convert_extent(box, geo_field):
# TODO: Parsing of BOX3D, Oracle support (patches welcome!)
# Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
# parsing out and returning as a 4-tuple.
ll, ur = box[4:-1].split(',')
xmin, ymin = map(float, ll.split())
xmax, ymax = map(float, ur.split())
return (xmin, ymin, xmax, ymax)
elif SpatialBackend.oracle:
def convert_extent(wkt, geo_field):
raise NotImplementedError
return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs)
# Getting the GeometryField and quoted column.
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('Extent information only available on GeometryFields.')
geo_col = self.query._field_column(geo_field)
# Constructing the query that will select the extent.
extent_sql = '%s(%s)' % (EXTENT, geo_col)
self.query.select = [GeomSQL(extent_sql)]
self.query.select_fields = [None]
try:
esql, params = self.query.as_sql()
except sql.datastructures.EmptyResultSet:
return None
# Getting a cursor, executing the query, and extracting the returned
# value from the extent function.
cursor = connection.cursor()
cursor.execute(esql, params)
box = cursor.fetchone()[0]
if box:
# TODO: Parsing of BOX3D, Oracle support (patches welcome!)
# Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
# parsing out and returning as a 4-tuple.
ll, ur = box[4:-1].split(',')
xmin, ymin = map(float, ll.split())
xmax, ymax = map(float, ur.split())
return (xmin, ymin, xmax, ymax)
else:
return None
def gml(self, field_name=None, precision=8, version=2):
def gml(self, precision=8, version=2, **kwargs):
"""
Returns GML representation of the given field in a `gml` attribute
on each element of the GeoQuerySet.
"""
# Is GML output supported?
ASGML = SpatialBackend.as_gml
if not ASGML:
raise ImproperlyConfigured('AsGML() stored procedure not available.')
# Getting the GeometryField and quoted column.
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('GML output only available on GeometryFields.')
geo_col = self.query._field_column(geo_field)
if SpatialBackend.oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
elif SpatialBackend.postgis:
s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
if SpatialBackend.postgis:
# PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh.
major, minor1, minor2 = SpatialBackend.version
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, geo_col, precision)}
procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
else:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, geo_col, precision, version)}
procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
s['procedure_args'] = {'precision' : precision, 'version' : version}
# Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select)
return self._spatial_attribute('gml', s, **kwargs)
def kml(self, field_name=None, precision=8):
def intersection(self, geom, **kwargs):
"""
Returns KML representation of the given field name in a `kml`
attribute on each element of the GeoQuerySet.
Returns the spatial intersection of the Geometry field in
an `intersection` attribute on each element of this
GeoQuerySet.
"""
# Is KML output supported?
ASKML = SpatialBackend.as_kml
if not ASKML:
raise ImproperlyConfigured('AsKML() stored procedure not available.')
return self._geomset_attribute('intersection', geom, **kwargs)
# Getting the GeometryField and quoted column.
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('KML output only available on GeometryFields.')
def kml(self, **kwargs):
"""
Returns KML representation of the geometry field in a `kml`
attribute on each element of this GeoQuerySet.
"""
s = {'desc' : 'KML',
'procedure_fmt' : '%(geo_col)s,%(precision)s',
'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
}
return self._spatial_attribute('kml', s, **kwargs)
geo_col = self.query._field_column(geo_field)
def length(self, **kwargs):
"""
Returns the length of the geometry field as a `Distance` object
stored in a `length` attribute on each element of this GeoQuerySet.
"""
return self._distance_attribute('length', None, **kwargs)
# Adding the AsKML function call to SELECT part of the SQL.
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
def make_line(self, **kwargs):
"""
Creates a linestring from all of the PointField geometries in the
this GeoQuerySet and returns it. This is a spatial aggregate
method, and thus returns a geometry rather than a GeoQuerySet.
"""
kwargs['geo_field_type'] = PointField
kwargs['agg_field'] = GeometryField
return self._spatial_aggregate('make_line', **kwargs)
def transform(self, field_name=None, srid=4326):
def mem_size(self, **kwargs):
"""
Returns the memory size (number of bytes) that the geometry field takes
in a `mem_size` attribute on each element of this GeoQuerySet.
"""
return self._spatial_attribute('mem_size', {}, **kwargs)
def num_geom(self, **kwargs):
"""
Returns the number of geometries if the field is a
GeometryCollection or Multi* Field in a `num_geom`
attribute on each element of this GeoQuerySet; otherwise
the sets with None.
"""
return self._spatial_attribute('num_geom', {}, **kwargs)
def num_points(self, **kwargs):
"""
Returns the number of points in the first linestring in the
Geometry field in a `num_points` attribute on each element of
this GeoQuerySet; otherwise sets with None.
"""
return self._spatial_attribute('num_points', {}, **kwargs)
def perimeter(self, **kwargs):
"""
Returns the perimeter of the geometry field as a `Distance` object
stored in a `perimeter` attribute on each element of this GeoQuerySet.
"""
return self._distance_attribute('perimeter', None, **kwargs)
def point_on_surface(self, **kwargs):
"""
Returns a Point geometry guaranteed to lie on the surface of the
Geometry field in a `point_on_surface` attribute on each element
of this GeoQuerySet; otherwise sets with None.
"""
return self._geom_attribute('point_on_surface', **kwargs)
def scale(self, x, y, z=0.0, **kwargs):
"""
Scales the geometry to a new size by multiplying the ordinates
with the given x,y,z scale factors.
"""
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
return self._spatial_attribute('scale', s, **kwargs)
def svg(self, **kwargs):
"""
Returns SVG representation of the geographic field in a `svg`
attribute on each element of this GeoQuerySet.
"""
s = {'desc' : 'SVG',
'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)),
'precision' : kwargs.pop('precision', 8)},
}
return self._spatial_attribute('svg', s, **kwargs)
def sym_difference(self, geom, **kwargs):
"""
Returns the symmetric difference of the geographic field in a
`sym_difference` attribute on each element of this GeoQuerySet.
"""
return self._geomset_attribute('sym_difference', geom, **kwargs)
def translate(self, x, y, z=0.0, **kwargs):
"""
Translates the geometry to a new location using the given numeric
parameters as offsets.
"""
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
return self._spatial_attribute('translate', s, **kwargs)
def transform(self, srid=4326, **kwargs):
"""
Transforms the given geometry field to the given SRID. If no SRID is
provided, the transformation will default to using 4326 (WGS84).
"""
# Getting the geographic field.
TRANSFORM = SpatialBackend.transform
if not TRANSFORM:
raise ImproperlyConfigured('Transform stored procedure not available.')
if not isinstance(srid, (int, long)):
raise TypeError('An integer SRID must be provided.')
field_name = kwargs.get('field_name', None)
tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
# `field_name` is first for backwards compatibility; but we want to
# be able to take integer srid as first parameter.
if isinstance(field_name, (int, long)):
srid = field_name
field_name = None
# Getting the GeometryField and quoted column.
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
# Getting the selection SQL for the given geograph
# Getting the selection SQL for the given geographic field.
field_col = self._geocol_select(geo_field, field_name)
# Why cascading substitutions? Because spatial backends like
@ -206,63 +268,334 @@ class GeoQuerySet(QuerySet):
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
if SpatialBackend.oracle:
custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
self.query.ewkt = srid
else:
custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
# TODO: Should we have this as an alias?
# custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
self.query.transformed_srid = srid # So other GeoQuerySet methods
self.query.custom_select[geo_field] = custom_sel
return self._clone()
def union(self, field_name=None, tolerance=0.0005):
def union(self, geom, **kwargs):
"""
Returns the union of the geographic field with the given
Geometry in a `union` attribute on each element of this GeoQuerySet.
"""
return self._geomset_attribute('union', geom, **kwargs)
def unionagg(self, **kwargs):
"""
Performs an aggregate union on the given geometry field. Returns
None if the GeoQuerySet is empty. The `tolerance` keyword is for
Oracle backends only.
"""
# Making sure backend supports the Union stored procedure
UNION = SpatialBackend.union
if not UNION:
raise ImproperlyConfigured('Union stored procedure not available.')
kwargs['agg_field'] = GeometryField
return self._spatial_aggregate('unionagg', **kwargs)
# Getting the GeometryField and quoted column.
### Private API -- Abstracted DRY routines. ###
def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None):
"""
Performs set up for executing the spatial function.
"""
# Does the spatial backend support this?
func = getattr(SpatialBackend, att, False)
if desc is None: desc = att
if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
# Initializing the procedure arguments.
procedure_args = {'function' : func}
# Is there a geographic field in the model to perform this
# operation on?
geo_field = self.query._geo_field(field_name)
if not geo_field:
raise TypeError('Aggregate Union only available on GeometryFields.')
geo_col = self.query._field_column(geo_field)
raise TypeError('%s output only available on GeometryFields.' % func)
# If the `geo_field_type` keyword was used, then enforce that
# type limitation.
if not geo_field_type is None and not isinstance(geo_field, geo_field_type):
raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__))
# Setting the procedure args.
procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
return procedure_args, geo_field
def _spatial_aggregate(self, att, field_name=None,
agg_field=None, convert_func=None,
geo_field_type=None, tolerance=0.0005):
"""
DRY routine for calling aggregate spatial stored procedures and
returning their result to the caller of the function.
"""
# Constructing the setup keyword arguments.
setup_kwargs = {'aggregate' : True,
'field_name' : field_name,
'geo_field_type' : geo_field_type,
}
procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs)
# Replacing the select with a call to the ST_Union stored procedure
# on the geographic field column.
if SpatialBackend.oracle:
union_sql = '%s' % SpatialBackend.select
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance))
procedure_args['tolerance'] = tolerance
# Adding in selection SQL for Oracle geometry columns.
if agg_field is GeometryField:
agg_sql = '%s' % SpatialBackend.select
else:
agg_sql = '%s'
agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args)
else:
union_sql = '%s(%s)' % (UNION, geo_col)
agg_sql = '%(function)s(%(geo_col)s)' % procedure_args
# Wrapping our selection SQL in `GeomSQL` to bypass quoting, and
# specifying the type of the aggregate field.
self.query.select = [GeomSQL(agg_sql)]
self.query.select_fields = [agg_field]
# Only want the union SQL to be selected.
self.query.select = [GeomSQL(union_sql)]
self.query.select_fields = [GeometryField]
try:
usql, params = self.query.as_sql()
# `asql` => not overriding `sql` module.
asql, params = self.query.as_sql()
except sql.datastructures.EmptyResultSet:
return None
# Getting a cursor, executing the query.
# Getting a cursor, executing the query, and extracting the returned
# value from the aggregate function.
cursor = connection.cursor()
cursor.execute(usql, params)
if SpatialBackend.oracle:
# On Oracle have to read out WKT from CLOB first.
clob = cursor.fetchone()[0]
if clob: u = clob.read()
else: u = None
cursor.execute(asql, params)
result = cursor.fetchone()[0]
# If the `agg_field` is specified as a GeometryField, then autmatically
# set up the conversion function.
if agg_field is GeometryField and not callable(convert_func):
if SpatialBackend.postgis:
def convert_geom(hex, geo_field):
if hex: return SpatialBackend.Geometry(hex)
else: return None
elif SpatialBackend.oracle:
def convert_geom(clob, geo_field):
if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
else: return None
convert_func = convert_geom
# Returning the callback function evaluated on the result culled
# from the executed cursor.
if callable(convert_func):
return convert_func(result, geo_field)
else:
u = cursor.fetchone()[0]
return result
if u: return GEOSGeometry(u)
else: return None
def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
"""
DRY routine for calling a spatial stored procedure on a geometry column
and attaching its output as an attribute of the model.
# Private API utilities, subject to change.
def _geocol_select(self, geo_field, field_name):
Arguments:
att:
The name of the spatial attribute that holds the spatial
SQL function to call.
settings:
Dictonary of internal settings to customize for the spatial procedure.
Public Keyword Arguments:
field_name:
The name of the geographic field to call the spatial
function on. May also be a lookup to a geometry field
as part of a foreign key relation.
model_att:
The name of the model attribute to attach the output of
the spatial function to.
"""
# Default settings.
settings.setdefault('desc', None)
settings.setdefault('geom_args', ())
settings.setdefault('geom_field', None)
settings.setdefault('procedure_args', {})
settings.setdefault('procedure_fmt', '%(geo_col)s')
settings.setdefault('select_params', [])
# Performing setup for the spatial column, unless told not to.
if settings.get('setup', True):
default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
else:
geo_field = settings['geo_field']
# The attribute to attach to the model.
if not isinstance(model_att, basestring): model_att = att
# Special handling for any argument that is a geometry.
for name in settings['geom_args']:
# Using the field's get_db_prep_lookup() to get any needed
# transformation SQL -- we pass in a 'dummy' `contains` lookup.
where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
# Replacing the procedure format with that of any needed
# transformation SQL.
old_fmt = '%%(%s)s' % name
new_fmt = where[0] % '%%s'
settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
settings['select_params'].extend(params)
# Getting the format for the stored procedure.
fmt = '%%(function)s(%s)' % settings['procedure_fmt']
# If the result of this function needs to be converted.
if settings.get('select_field', False):
sel_fld = settings['select_field']
if isinstance(sel_fld, GeomField) and SpatialBackend.select:
self.query.custom_select[model_att] = SpatialBackend.select
self.query.extra_select_fields[model_att] = sel_fld
# Finally, setting the extra selection attribute with
# the format string expanded with the stored procedure
# arguments.
return self.extra(select={model_att : fmt % settings['procedure_args']},
select_params=settings['select_params'])
def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
"""
DRY routine for GeoQuerySet distance attribute routines.
"""
# Setting up the distance procedure arguments.
procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
# If geodetic defaulting distance attribute to meters (Oracle and
# PostGIS spherical distances return meters). Otherwise, use the
# units of the geometry field.
if geo_field.geodetic:
dist_att = 'm'
else:
dist_att = Distance.unit_attname(geo_field._unit_name)
# Shortcut booleans for what distance function we're using.
distance = func == 'distance'
length = func == 'length'
perimeter = func == 'perimeter'
if not (distance or length or perimeter):
raise ValueError('Unknown distance function: %s' % func)
# The field's get_db_prep_lookup() is used to get any
# extra distance parameters. Here we set up the
# parameters that will be passed in to field's function.
lookup_params = [geom or 'POINT (0 0)', 0]
# If the spheroid calculation is desired, either by the `spheroid`
# keyword or wehn calculating the length of geodetic field, make
# sure the 'spheroid' distance setting string is passed in so we
# get the correct spatial stored procedure.
if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
lookup_params.append('spheroid')
where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
# The `geom_args` flag is set to true if a geometry parameter was
# passed in.
geom_args = bool(geom)
if SpatialBackend.oracle:
if distance:
procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
elif length or perimeter:
procedure_fmt = '%(geo_col)s,%(tolerance)s'
procedure_args['tolerance'] = tolerance
else:
# Getting whether this field is in units of degrees since the field may have
# been transformed via the `transform` GeoQuerySet method.
if self.query.transformed_srid:
u, unit_name, s = get_srid_info(self.query.transformed_srid)
geodetic = unit_name in geo_field.geodetic_units
else:
geodetic = geo_field.geodetic
if distance:
if self.query.transformed_srid:
# Setting the `geom_args` flag to false because we want to handle
# transformation SQL here, rather than the way done by default
# (which will transform to the original SRID of the field rather
# than to what was transformed to).
geom_args = False
procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
if geom.srid is None or geom.srid == self.query.transformed_srid:
# If the geom parameter srid is None, it is assumed the coordinates
# are in the transformed units. A placeholder is used for the
# geometry parameter.
procedure_fmt += ', %%s'
else:
# We need to transform the geom to the srid specified in `transform()`,
# so wrapping the geometry placeholder in transformation SQL.
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
else:
# `transform()` was not used on this GeoQuerySet.
procedure_fmt = '%(geo_col)s,%(geom)s'
if geodetic:
# Spherical distance calculation is needed (because the geographic
# field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
# procedures may only do queries from point columns to point geometries
# some error checking is required.
if not isinstance(geo_field, PointField):
raise TypeError('Spherical distance calculation only supported on PointFields.')
if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
# The `function` procedure argument needs to be set differently for
# geodetic distance calculations.
if spheroid:
# Call to distance_spheroid() requires spheroid param as well.
procedure_fmt += ',%(spheroid)s'
procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
else:
procedure_args.update({'function' : SpatialBackend.distance_sphere})
elif length or perimeter:
procedure_fmt = '%(geo_col)s'
if geodetic and length:
# There's no `length_sphere`
procedure_fmt += ',%(spheroid)s'
procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
# Setting up the settings for `_spatial_attribute`.
s = {'select_field' : DistanceField(dist_att),
'setup' : False,
'geo_field' : geo_field,
'procedure_args' : procedure_args,
'procedure_fmt' : procedure_fmt,
}
if geom_args:
s['geom_args'] = ('geom',)
s['procedure_args']['geom'] = geom
elif geom:
# The geometry is passed in as a parameter because we handled
# transformation conditions in this routine.
s['select_params'] = [SpatialBackend.Adaptor(geom)]
return self._spatial_attribute(func, s, **kwargs)
def _geom_attribute(self, func, tolerance=0.05, **kwargs):
"""
DRY routine for setting up a GeoQuerySet method that attaches a
Geometry attribute (e.g., `centroid`, `point_on_surface`).
"""
s = {'select_field' : GeomField(),}
if SpatialBackend.oracle:
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args'] = {'tolerance' : tolerance}
return self._spatial_attribute(func, s, **kwargs)
def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
"""
DRY routine for setting up a GeoQuerySet method that attaches a
Geometry attribute and takes a Geoemtry parameter. This is used
for geometry set-like operations (e.g., intersection, difference,
union, sym_difference).
"""
s = {'geom_args' : ('geom',),
'select_field' : GeomField(),
'procedure_fmt' : '%(geo_col)s,%(geom)s',
'procedure_args' : {'geom' : geom},
}
if SpatialBackend.oracle:
s['procedure_fmt'] += ',%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance
return self._spatial_attribute(func, s, **kwargs)
def _geocol_select(self, geo_field, field_name, aggregate=False):
"""
Helper routine for constructing the SQL to select the geographic
column. Takes into account if the geographic field is in a
@ -274,6 +607,8 @@ class GeoQuerySet(QuerySet):
# (e.g., if 'location__point' was given as the field name).
self.query.add_select_related([field_name])
self.query.pre_sql_setup()
# Can't non-aggregate and aggregate selections together.
if aggregate: self.query.aggregate = True
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
return self.query._field_column(geo_field, rel_table)
else:

View File

@ -1,2 +1,2 @@
from django.contrib.gis.db.models.sql.query import GeoQuery
from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery
from django.contrib.gis.db.models.sql.where import GeoWhereNode

View File

@ -6,6 +6,7 @@ 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()
@ -24,14 +25,19 @@ class GeoQuery(sql.Query):
# 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.ewkt = None
self.transformed_srid = None
self.extra_select_fields = {}
def clone(self, *args, **kwargs):
obj = super(GeoQuery, self).clone(*args, **kwargs)
# Customized selection dictionary and EWKT flag have to be added to obj.
# 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.ewkt = self.ewkt
obj.transformed_srid = self.transformed_srid
obj.extra_select_fields = self.extra_select_fields.copy()
return obj
def get_columns(self, with_aliases=False):
@ -49,7 +55,8 @@ class GeoQuery(sql.Query):
"""
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
result = ['(%s) AS %s' % (col, qn2(alias)) for alias, col in self.extra_select.iteritems()]
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col, qn2(alias))
for alias, col in self.extra_select.iteritems()]
aliases = set(self.extra_select.keys())
if with_aliases:
col_aliases = aliases.copy()
@ -80,17 +87,18 @@ class GeoQuery(sql.Query):
result.extend(cols)
aliases.update(new_aliases)
# This loop customized for GeoQuery.
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)
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
@ -139,7 +147,51 @@ class GeoQuery(sql.Query):
col_aliases.add(field.column)
return result, aliases
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
@ -173,8 +225,8 @@ class GeoQuery(sql.Query):
# 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.ewkt and SpatialBackend.oracle:
sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt)
if self.transformed_srid and SpatialBackend.oracle:
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
else:
sel_fmt = '%s'
return sel_fmt
@ -188,8 +240,8 @@ class GeoQuery(sql.Query):
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 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
@ -242,3 +294,17 @@ class GeoQuery(sql.Query):
# 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

View File

@ -1,5 +1,5 @@
from django.db.models.sql.where import WhereNode
from django.contrib.gis.db.backend import get_geo_where_clause, GIS_TERMS
from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
class GeoWhereNode(WhereNode):
"""
@ -9,7 +9,7 @@ class GeoWhereNode(WhereNode):
def make_atom(self, child, qn):
table_alias, name, field, lookup_type, value = child
if hasattr(field, '_geom'):
if lookup_type in GIS_TERMS:
if lookup_type in SpatialBackend.gis_terms:
# Getting the geographic where clause; substitution parameters
# will be populated in the GeoFieldSQL object returned by the
# GeometryField.

View File

@ -30,14 +30,60 @@
Distance and Area objects to allow for sensible and convienient calculation
and conversions.
Author: Robert Coup
Author: Robert Coup, Justin Bronn
Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
and Geoff Biggs' PhD work on dimensioned units for robotics.
"""
__all__ = ['A', 'Area', 'D', 'Distance']
from decimal import Decimal
class Distance(object):
class MeasureBase(object):
def default_units(self, kwargs):
"""
Return the unit value and the the default units specified
from the given keyword arguments dictionary.
"""
val = 0.0
for unit, value in kwargs.iteritems():
if unit in self.UNITS:
val += self.UNITS[unit] * value
default_unit = unit
elif unit in self.ALIAS:
u = self.ALIAS[unit]
val += self.UNITS[u] * value
default_unit = u
else:
lower = unit.lower()
if lower in self.UNITS:
val += self.UNITS[lower] * value
default_unit = lower
elif lower in self.LALIAS:
u = self.LALIAS[lower]
val += self.UNITS[u] * value
default_unit = u
else:
raise AttributeError('Unknown unit type: %s' % unit)
return val, default_unit
@classmethod
def unit_attname(cls, unit_str):
"""
Retrieves the unit attribute name for the given unit string.
For example, if the given unit string is 'metre', 'm' would be returned.
An exception is raised if an attribute cannot be found.
"""
lower = unit_str.lower()
if unit_str in cls.UNITS:
return unit_str
elif lower in cls.UNITS:
return lower
elif lower in cls.LALIAS:
return cls.LALIAS[lower]
else:
raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
class Distance(MeasureBase):
UNITS = {
'chain' : 20.1168,
'chain_benoit' : 20.116782,
@ -53,7 +99,6 @@ class Distance(object):
'fathom' : 1.8288,
'ft': 0.3048,
'german_m' : 1.0000135965,
'grad' : 0.0157079632679,
'gold_coast_ft' : 0.304799710181508,
'indian_yd' : 0.914398530744,
'in' : 0.0254,
@ -92,9 +137,10 @@ class Distance(object):
'British chain (Sears 1922)' : 'british_chain_sears',
'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
'British foot (Sears 1922)' : 'british_ft',
'British foot' : 'british_ft',
'British yard (Sears 1922)' : 'british_yd',
'British yard' : 'british_yd',
"Clarke's Foot" : 'clarke_ft',
"Clarke's foot" : 'clarke_ft',
"Clarke's link" : 'clarke_link',
'Chain (Benoit)' : 'chain_benoit',
'Chain (Sears)' : 'chain_sears',
@ -111,33 +157,11 @@ class Distance(object):
'Yard (Indian)' : 'indian_yd',
'Yard (Sears)' : 'sears_yd'
}
REV_ALIAS = dict((value, key) for key, value in ALIAS.items())
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
def __init__(self, default_unit=None, **kwargs):
# The base unit is in meters.
self.m = 0.0
self._default_unit = 'm'
for unit,value in kwargs.items():
if unit in self.UNITS:
self.m += self.UNITS[unit] * value
self._default_unit = unit
elif unit in self.ALIAS:
u = self.ALIAS[unit]
self.m += self.UNITS[u] * value
self._default_unit = u
else:
lower = unit.lower()
if lower in self.UNITS:
self.m += self.UNITS[lower] * value
self._default_unit = lower
elif lower in self.ALIAS:
u = self.ALIAS[lower]
self.m += self.UNITS[u] * value
self._default_unit = u
else:
raise AttributeError('Unknown unit type: %s' % unit)
self.m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit
@ -216,49 +240,15 @@ class Distance(object):
def __nonzero__(self):
return bool(self.m)
@classmethod
def unit_attname(cls, unit_str):
"""
Retrieves the unit attribute name for the given unit string.
For example, if the given unit string is 'metre', 'm' would be returned.
An exception is raised if an attribute cannot be found.
"""
lower = unit_str.lower()
if unit_str in cls.UNITS:
return unit_str
elif lower in cls.UNITS:
return lower
elif unit_str in cls.ALIAS:
return cls.ALIAS[unit_str]
elif lower in cls.ALIAS:
return cls.ALIAS[lower]
else:
raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
class Area(object):
# TODO: Add units from above.
UNITS = {
'sq_m': 1.0,
'sq_km': 1000000.0,
'sq_mi': 2589988.110336,
'sq_ft': 0.09290304,
'sq_yd': 0.83612736,
'sq_nm': 3429904.0,
}
class Area(MeasureBase):
# Getting the square units values and the alias dictionary.
UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()])
ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()])
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
def __init__(self, default_unit=None, **kwargs):
self.sq_m = 0.0
self._default_unit = 'sq_m'
for unit,value in kwargs.items():
if unit in self.UNITS:
self.sq_m += self.UNITS[unit] * value
self._default_unit = unit
else:
raise AttributeError('Unknown unit type: ' + unit)
if default_unit:
self.sq_m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit
def __getattr__(self, name):
@ -334,7 +324,6 @@ class Area(object):
def __nonzero__(self):
return bool(self.sq_m)
# Shortcuts
D = Distance
A = Area

View File

@ -209,9 +209,54 @@ class SpatialRefSysMixin(object):
return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models
_srid_info = True
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
else:
pass
_srid_info = False
if _srid_info:
def get_srid_info(srid):
"""
Returns the units, unit name, and spheroid WKT associated with the
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
table. We use a database cursor to execute the query because this
function is used when it is not possible to use the ORM (for example,
during field initialization).
"""
from django.db import connection
# Getting the spatial reference WKT associated with the SRID from the
# `spatial_ref_sys` (or equivalent) spatial database table.
#
# The following doesn't work: SpatialRefSys.objects.get(srid=srid)
# Why? `syncdb` fails to recognize installed geographic models when there's
# an ORM query instantiated within a model field.
cur = connection.cursor()
qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
'wkt_col' : qn(SpatialRefSys.wkt_col()),
'srid_col' : qn('srid'),
'srid' : srid,
}
cur.execute(stmt)
srs_wkt = cur.fetchone()[0]
if srs_wkt is None:
raise ValueError('Failed to find Spatial Reference System entry corresponding to SRID=%s' % srid)
# Getting metadata associated with the spatial reference system identifier.
# Specifically, getting the unit information and spheroid information
# (both required for distance queries).
unit, unit_name = SpatialRefSys.get_units(srs_wkt)
spheroid = SpatialRefSys.get_spheroid(srs_wkt)
return unit, unit_name, spheroid
else:
def get_srid_info(srid):
"""
Dummy routine for the backends that do not have the OGC required
spatial metadata tables (like MySQL).
"""
return None, None, None

View File

@ -11,7 +11,7 @@ from django.conf import settings
if not settings._target: settings.configure()
# Tests that require use of a spatial database (e.g., creation of models)
test_models = ['geoapp', 'relatedapp']
test_models = ['geoapp',]
# Tests that do not require setting up and tearing down a spatial database.
test_suite_names = [
@ -20,11 +20,14 @@ test_suite_names = [
]
if HAS_GDAL:
if oracle:
# TODO: There is a problem with the `syncdb` SQL for the LayerMapping
# tests on Oracle.
test_models += ['distapp']
elif postgis:
# TODO: There's a problem with `select_related` and GeoQuerySet on
# Oracle -- e.g., GeoModel.objects.distance(geom, field_name='fk__point')
# doesn't work so we don't test `relatedapp`.
test_models += ['distapp', 'layermap']
elif postgis:
test_models += ['distapp', 'layermap', 'relatedapp']
elif mysql:
test_models += ['relatedapp']
test_suite_names += [
'test_gdal_driver',

View File

@ -11,16 +11,23 @@ au_cities = (('Wollongong', 150.902, -34.4245),
('Hillsdale', 151.231341, -33.952685),
)
stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),
('West University Place', 943547.922328, 4213623.65345),
('Southside Place', 944704.643307, 4212768.87617),
('Bellaire', 942595.669129, 4212686.72583),
('Pearland', 959674.616506, 4197465.6526),
('Galveston', 1008151.16007, 4170027.47655),
('Sealy', 874859.286808, 4219186.8641),
('San Antonio', 649173.910483, 4176413.27786),
('Round Rock', 726846.03695, 4297160.99715),
('Saint Hedwig', 677644.649952, 4175467.06744),
stx_cities = (('Downtown Houston', -95.363151, 29.763374),
('West University Place', -95.448601, 29.713803),
('Southside Place', -95.436920, 29.705777),
('Bellaire', -95.458732, 29.705614),
('Pearland', -95.287303, 29.563568),
('Galveston', -94.797489, 29.301336),
('Sealy', -96.156952, 29.780918),
('San Antonio', -98.493183, 29.424170),
('Saint Hedwig', -98.199820, 29.414197),
)
# Data from U.S. Census ZCTA cartographic boundary file for Texas (`zt48_d00.shp`).
stx_zips = (('77002', 'POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95.360915 29.771827, -95.354615 29.771827, -95.351515 29.772527, -95.350915 29.765327, -95.351015 29.762436, -95.350115 29.760328, -95.347515 29.758528, -95.352315 29.753928, -95.356415 29.756328, -95.358215 29.754028, -95.360215 29.756328, -95.363415 29.757128, -95.364014 29.75638, -95.363415 29.753928, -95.360015 29.751828, -95.361815 29.749528, -95.362715 29.750028, -95.367516 29.744128, -95.369316 29.745128, -95.373916 29.744128, -95.380116 29.738028, -95.387916 29.727929, -95.388516 29.729629, -95.387916 29.732129, -95.382916 29.737428, -95.376616 29.742228, -95.372616 29.747228, -95.378601 29.750846, -95.378616 29.752028, -95.378616 29.754428, -95.376016 29.754528, -95.374616 29.759828, -95.373616 29.761128, -95.371916 29.763928, -95.372316 29.768727, -95.365884 29.76791, -95.366015 29.767127, -95.358715 29.765327, -95.358615 29.766327, -95.359115 29.767227, -95.360215 29.767027, -95.362783 29.768267, -95.365315 29.770527, -95.365015 29.772327))'),
('77005', 'POLYGON ((-95.447918 29.727275, -95.428017 29.728729, -95.421117 29.729029, -95.418617 29.727629, -95.418517 29.726429, -95.402117 29.726629, -95.402117 29.725729, -95.395316 29.725729, -95.391916 29.726229, -95.389716 29.725829, -95.396517 29.715429, -95.397517 29.715929, -95.400917 29.711429, -95.411417 29.715029, -95.418417 29.714729, -95.418317 29.70623, -95.440818 29.70593, -95.445018 29.70683, -95.446618 29.70763, -95.447418 29.71003, -95.447918 29.727275))'),
('77025', 'POLYGON ((-95.418317 29.70623, -95.414717 29.706129, -95.414617 29.70533, -95.418217 29.70533, -95.419817 29.69533, -95.419484 29.694196, -95.417166 29.690901, -95.414517 29.69433, -95.413317 29.69263, -95.412617 29.68973, -95.412817 29.68753, -95.414087 29.685055, -95.419165 29.685428, -95.421617 29.68513, -95.425717 29.67983, -95.425017 29.67923, -95.424517 29.67763, -95.427418 29.67763, -95.438018 29.664631, -95.436713 29.664411, -95.440118 29.662231, -95.439218 29.661031, -95.437718 29.660131, -95.435718 29.659731, -95.431818 29.660331, -95.441418 29.656631, -95.441318 29.656331, -95.441818 29.656131, -95.441718 29.659031, -95.441118 29.661031, -95.446718 29.656431, -95.446518 29.673431, -95.446918 29.69013, -95.447418 29.71003, -95.446618 29.70763, -95.445018 29.70683, -95.440818 29.70593, -95.418317 29.70623))'),
('77401', 'POLYGON ((-95.447918 29.727275, -95.447418 29.71003, -95.446918 29.69013, -95.454318 29.68893, -95.475819 29.68903, -95.475819 29.69113, -95.484419 29.69103, -95.484519 29.69903, -95.480419 29.70133, -95.480419 29.69833, -95.474119 29.69833, -95.474119 29.70453, -95.472719 29.71283, -95.468019 29.71293, -95.468219 29.720229, -95.464018 29.720229, -95.464118 29.724529, -95.463018 29.725929, -95.459818 29.726129, -95.459918 29.720329, -95.451418 29.720429, -95.451775 29.726303, -95.451318 29.727029, -95.447918 29.727275))'),
)
interstates = (('I-25', 'LINESTRING(-104.4780170766108 36.66698791870694, -104.4468522338495 36.79925409393386, -104.46212692626 36.9372149776075, -104.5126119783768 37.08163268820887, -104.5247764602161 37.29300499892048, -104.7084397427668 37.49150259925398, -104.8126599016282 37.69514285621863, -104.8452887035466 37.87613395659479, -104.7160169341003 38.05951763337799, -104.6165437927668 38.30432045855106, -104.6437227858174 38.53979986564737, -104.7596170387259 38.7322907594295, -104.8380078676822 38.89998460604341, -104.8501253693506 39.09980189213358, -104.8791648316464 39.24368776457503, -104.8635041274215 39.3785278162751, -104.8894471170052 39.5929228239605, -104.9721242843344 39.69528482419685, -105.0112104500356 39.7273080432394, -105.0010368577104 39.76677607811571, -104.981835619 39.81466504121967, -104.9858891550477 39.88806911250832, -104.9873548059578 39.98117234571016, -104.9766220487419 40.09796423450692, -104.9818565932953 40.36056530662884, -104.9912746373997 40.74904484447656)'),
)

View File

@ -7,6 +7,13 @@ class SouthTexasCity(models.Model):
objects = models.GeoManager()
def __unicode__(self): return self.name
class SouthTexasCityFt(models.Model):
"Same City model as above, but U.S. survey feet are the units."
name = models.CharField(max_length=30)
point = models.PointField(srid=2278)
objects = models.GeoManager()
def __unicode__(self): return self.name
class AustraliaCity(models.Model):
"City model for Australia, using WGS84."
name = models.CharField(max_length=30)
@ -14,7 +21,22 @@ class AustraliaCity(models.Model):
objects = models.GeoManager()
def __unicode__(self): return self.name
#class County(models.Model):
# name = models.CharField(max_length=30)
# mpoly = models.MultiPolygonField(srid=32140)
# objects = models.GeoManager()
class CensusZipcode(models.Model):
"Model for a few South Texas ZIP codes (in original Census NAD83)."
name = models.CharField(max_length=5)
poly = models.PolygonField(srid=4269)
objects = models.GeoManager()
class SouthTexasZipcode(models.Model):
"Model for a few South Texas ZIP codes."
name = models.CharField(max_length=5)
poly = models.PolygonField(srid=32140)
objects = models.GeoManager()
def __unicode__(self): return self.name
class Interstate(models.Model):
"Geodetic model for U.S. Interstates."
name = models.CharField(max_length=10)
line = models.LineStringField()
objects = models.GeoManager()
def __unicode__(self): return self.name

View File

@ -1,14 +1,15 @@
import os, unittest
from decimal import Decimal
from django.db.models import Q
from django.contrib.gis.gdal import DataSource
from django.contrib.gis.geos import GEOSGeometry, Point, LineString
from django.contrib.gis.measure import D # alias for Distance
from django.contrib.gis.db.models import GeoQ
from django.contrib.gis.tests.utils import oracle
from django.contrib.gis.tests.utils import oracle, postgis, no_oracle
from models import SouthTexasCity, AustraliaCity
from data import au_cities, stx_cities
from models import AustraliaCity, Interstate, SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode
from data import au_cities, interstates, stx_cities, stx_zips
class DistanceTest(unittest.TestCase):
@ -20,7 +21,7 @@ class DistanceTest(unittest.TestCase):
# Another one for Australia
au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326)
def get_cities(self, qs):
def get_names(self, qs):
cities = [c.name for c in qs]
cities.sort()
return cities
@ -28,33 +29,56 @@ class DistanceTest(unittest.TestCase):
def test01_init(self):
"Initialization of distance models."
def load_cities(city_model, srid, data_tup):
# Loading up the cities.
def load_cities(city_model, data_tup):
for name, x, y in data_tup:
c = city_model(name=name, point=Point(x, y, srid=srid))
c = city_model(name=name, point=Point(x, y, srid=4326))
c.save()
load_cities(SouthTexasCity, 32140, stx_cities)
load_cities(AustraliaCity, 4326, au_cities)
load_cities(SouthTexasCity, stx_cities)
load_cities(SouthTexasCityFt, stx_cities)
load_cities(AustraliaCity, au_cities)
self.assertEqual(10, SouthTexasCity.objects.count())
self.assertEqual(9, SouthTexasCity.objects.count())
self.assertEqual(9, SouthTexasCityFt.objects.count())
self.assertEqual(11, AustraliaCity.objects.count())
# Loading up the South Texas Zip Codes.
for name, wkt in stx_zips:
poly = GEOSGeometry(wkt, srid=4269)
SouthTexasZipcode(name=name, poly=poly).save()
CensusZipcode(name=name, poly=poly).save()
self.assertEqual(4, SouthTexasZipcode.objects.count())
self.assertEqual(4, CensusZipcode.objects.count())
# Loading up the Interstates.
for name, wkt in interstates:
Interstate(name=name, line=GEOSGeometry(wkt, srid=4326)).save()
self.assertEqual(1, Interstate.objects.count())
def test02_dwithin(self):
"Testing the `dwithin` lookup type."
# Distances -- all should be equal (except for the
# degree/meter pair in au_cities, that's somewhat
# approximate).
tx_dists = [7000, D(km=7), D(mi=4.349)]
tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)]
au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)]
# Expected cities for Australia and Texas.
tx_cities = ['Downtown Houston', 'Southside Place']
au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong']
# Performing distance queries on two projected coordinate systems one
# with units in meters and the other in units of U.S. survey feet.
for dist in tx_dists:
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
self.assertEqual(tx_cities, self.get_cities(qs))
if isinstance(dist, tuple): dist1, dist2 = dist
else: dist1 = dist2 = dist
qs1 = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist1))
qs2 = SouthTexasCityFt.objects.filter(point__dwithin=(self.stx_pnt, dist2))
for qs in qs1, qs2:
self.assertEqual(tx_cities, self.get_names(qs))
# Now performing the `dwithin` queries on a geodetic coordinate system.
for dist in au_dists:
if isinstance(dist, D) and not oracle: type_error = True
else: type_error = False
@ -70,23 +94,29 @@ class DistanceTest(unittest.TestCase):
# Distance objects into a DWithin query using a geodetic field.
self.assertRaises(TypeError, qs.count)
else:
self.assertEqual(au_cities, self.get_cities(qs))
self.assertEqual(au_cities, self.get_names(qs))
def test03_distance_aggregate(self):
"Testing the `distance` GeoQuerySet method."
def test03a_distance_method(self):
"Testing the `distance` GeoQuerySet method on projected coordinate systems."
# The point for La Grange, TX
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
# Got these from using the raw SQL statement:
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_southtexascity;
distances = [147075.069813, 139630.198056, 140888.552826,
138809.684197, 158309.246259, 212183.594374,
70870.188967, 165337.758878, 102128.654360,
139196.085105]
# Reference distances in feet and in meters. Got these values from
# using the provided raw SQL statements.
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140)) FROM distapp_southtexascity;
m_distances = [147075.069813, 139630.198056, 140888.552826,
138809.684197, 158309.246259, 212183.594374,
70870.188967, 165337.758878, 139196.085105]
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft;
ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
455411.438904354, 519386.252102563, 696139.009211594,
232513.278304279, 542445.630586414, 456679.155883207]
# Testing when the field name is explicitly set.
dist1 = SouthTexasCity.objects.distance('point', lagrange)
# Testing using different variations of parameters and using models
# with different projected coordinate systems.
dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point')
dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter
dist3 = SouthTexasCity.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
dist4 = SouthTexasCityFt.objects.distance(lagrange)
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle.
@ -94,9 +124,15 @@ class DistanceTest(unittest.TestCase):
else: tol = 5
# Ensuring expected distances are returned for each distance queryset.
for qs in [dist1, dist2, dist3]:
for qs in [dist1, dist2, dist3, dist4]:
for i, c in enumerate(qs):
self.assertAlmostEqual(distances[i], c.distance, tol)
self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
def test03b_distance_method(self):
"Testing the `distance` GeoQuerySet method on geodetic coordnate systems."
if oracle: tol = 2
else: tol = 5
# Now testing geodetic distance aggregation.
hillsdale = AustraliaCity.objects.get(name='Hillsdale')
@ -106,61 +142,153 @@ class DistanceTest(unittest.TestCase):
self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)')
self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1)))
# Got these distances using the raw SQL statement:
# Got the reference distances using the raw SQL statements:
# SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11));
geodetic_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647]
spheroid_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647]
# SELECT ST_distance_sphere(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326)) FROM distapp_australiacity WHERE (NOT (id = 11)); st_distance_sphere
sphere_distances = [60580.7612632291, 77143.7785056615, 49199.2725132184, 90804.4414289463, 217712.63666124, 709131.691061906, 639825.959074112, 7786.80274606706, 1049200.46122281, 1162619.7297006]
# Ensuring the expected distances are returned.
qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point)
# Testing with spheroid distances first.
qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point, spheroid=True)
for i, c in enumerate(qs):
self.assertAlmostEqual(geodetic_distances[i], c.distance, tol)
self.assertAlmostEqual(spheroid_distances[i], c.distance.m, tol)
if postgis:
# PostGIS uses sphere-only distances by default, testing these as well.
qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point)
for i, c in enumerate(qs):
self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol)
@no_oracle # Oracle already handles geographic distance calculation.
def test03c_distance_method(self):
"Testing the `distance` GeoQuerySet method used with `transform` on a geographic field."
# Normally you can't compute distances from a geometry field
# that is not a PointField (on PostGIS).
self.assertRaises(TypeError, CensusZipcode.objects.distance, self.stx_pnt)
# We'll be using a Polygon (created by buffering the centroid
# of 77005 to 100m) -- which aren't allowed in geographic distance
# queries normally, however our field has been transformed to
# a non-geographic system.
z = SouthTexasZipcode.objects.get(name='77005')
# Reference query:
# SELECT ST_Distance(ST_Transform("distapp_censuszipcode"."poly", 32140), ST_GeomFromText('<buffer_wkt>', 32140)) FROM "distapp_censuszipcode";
dists_m = [3553.30384972258, 1243.18391525602, 2186.15439472242]
# Having our buffer in the SRID of the transformation and of the field
# -- should get the same results. The first buffer has no need for
# transformation SQL because it is the same SRID as what was given
# to `transform()`. The second buffer will need to be transformed,
# however.
buf1 = z.poly.centroid.buffer(100)
buf2 = buf1.transform(4269, clone=True)
for buf in [buf1, buf2]:
qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf)
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
for i, z in enumerate(qs):
self.assertAlmostEqual(z.distance.m, dists_m[i], 5)
def test04_distance_lookups(self):
"Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
# Only two cities (Houston and Southside Place) should be
# within 7km of the given point.
dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
]
# Retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
# (thus, Houston and Southside place will be excluded as tested in
# the `test02_dwithin` above).
qs1 = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
for qs in qs1, qs2:
cities = self.get_names(qs)
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
for dist in dists:
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
for c in qs:
cities = self.get_cities(qs)
self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
# Doing a distance query using Polygons instead of a Point.
z = SouthTexasZipcode.objects.get(name='77005')
qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=275)))
self.assertEqual(['77025', '77401'], self.get_names(qs))
# If we add a little more distance 77002 should be included.
qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300)))
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
# Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
# (thus, Houston and Southside place will be excluded)
qs = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
cities = self.get_cities(qs)
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
def test05_geodetic_distance(self):
def test05_geodetic_distance_lookups(self):
"Testing distance lookups on geodetic coordinate systems."
if not oracle:
# Oracle doesn't have this limitation -- PostGIS only allows geodetic
# distance queries from Points to PointFields.
mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
self.assertRaises(TypeError,
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
# Too many params (4 in this case) should raise a ValueError.
self.assertRaises(ValueError,
AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')).count)
hobart = AustraliaCity.objects.get(name='Hobart')
# Not enough params should raise a ValueError.
self.assertRaises(ValueError,
AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)',)).count)
# Getting all cities w/in 550 miles of Hobart.
hobart = AustraliaCity.objects.get(name='Hobart')
qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550)))
cities = self.get_cities(qs)
cities = self.get_names(qs)
self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne'])
# Cities that are either really close or really far from Wollongong --
# and using different units of distance.
wollongong = AustraliaCity.objects.get(name='Wollongong')
gq1 = GeoQ(point__distance_lte=(wollongong.point, D(yd=19500))) # Yards (~17km)
gq2 = GeoQ(point__distance_gte=(wollongong.point, D(nm=400))) # Nautical Miles
qs = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
cities = self.get_cities(qs)
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles.
# Normal geodetic distance lookup (uses `distance_sphere` on PostGIS.
gq1 = GeoQ(point__distance_lte=(wollongong.point, d1))
gq2 = GeoQ(point__distance_gte=(wollongong.point, d2))
qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
# Geodetic distance lookup but telling GeoDjango to use `distance_spheroid`
# instead (we should get the same results b/c accuracy variance won't matter
# in this test case). Using `Q` instead of `GeoQ` to be different (post-qsrf
# it doesn't matter).
if postgis:
gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid'))
gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid'))
qs2 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq3 | gq4)
querysets = [qs1, qs2]
else:
querysets = [qs1]
for qs in querysets:
cities = self.get_names(qs)
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
def test06_area(self):
"Testing the `area` GeoQuerySet method."
# Reference queries:
# SELECT ST_Area(poly) FROM distapp_southtexaszipcode;
area_sq_m = [5437908.90234375, 10183031.4389648, 11254471.0073242, 9881708.91772461]
# Tolerance has to be lower for Oracle and differences
# with GEOS 3.0.0RC4
tol = 2
for i, z in enumerate(SouthTexasZipcode.objects.area()):
self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol)
def test07_length(self):
"Testing the `length` GeoQuerySet method."
# Reference query (should use `length_spheroid`).
# SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]');
len_m = 473504.769553813
qs = Interstate.objects.length()
if oracle: tol = 2
else: tol = 7
self.assertAlmostEqual(len_m, qs[0].length.m, tol)
def test08_perimeter(self):
"Testing the `perimeter` GeoQuerySet method."
# Reference query:
# SELECT ST_Perimeter(distapp_southtexaszipcode.poly) FROM distapp_southtexaszipcode;
perim_m = [18404.3550889361, 15627.2108551001, 20632.5588368978, 17094.5996143697]
if oracle: tol = 2
else: tol = 7
for i, z in enumerate(SouthTexasZipcode.objects.perimeter()):
self.assertAlmostEqual(perim_m[i], z.perimeter.m, tol)
# Running on points; should return 0.
for i, c in enumerate(SouthTexasCity.objects.perimeter(model_att='perim')):
self.assertEqual(0, c.perim.m)
def suite():
s = unittest.TestSuite()

View File

@ -28,14 +28,10 @@ class GeoModelTest(unittest.TestCase):
def get_file(wkt_file):
return os.path.join(data_dir, wkt_file)
co = State(name='Colorado', poly=fromfile(get_file('co.wkt')))
co.save()
ks = State(name='Kansas', poly=fromfile(get_file('ks.wkt')))
ks.save()
tx = Country(name='Texas', mpoly=fromfile(get_file('tx.wkt')))
tx.save()
nz = Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt')))
nz.save()
State(name='Colorado', poly=fromfile(get_file('co.wkt'))).save()
State(name='Kansas', poly=fromfile(get_file('ks.wkt'))).save()
Country(name='Texas', mpoly=fromfile(get_file('tx.wkt'))).save()
Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt'))).save()
# Ensuring that data was loaded from initial SQL.
self.assertEqual(2, Country.objects.count())
@ -49,7 +45,7 @@ class GeoModelTest(unittest.TestCase):
def test02_proxy(self):
"Testing Lazy-Geometry support (using the GeometryProxy)."
if DISABLE: return
#### Testing on a Point
## Testing on a Point
pnt = Point(0, 0)
nullcity = City(name='NullCity', point=pnt)
nullcity.save()
@ -86,7 +82,7 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point)
nullcity.delete()
#### Testing on a Polygon
## Testing on a Polygon
shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0))
inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40))
@ -136,7 +132,7 @@ class GeoModelTest(unittest.TestCase):
ref_kml = ref_kml2
# Ensuring the KML is as expected.
ptown1 = City.objects.kml('point', precision=9).get(name='Pueblo')
ptown1 = City.objects.kml(field_name='point', precision=9).get(name='Pueblo')
ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
for ptown in [ptown1, ptown2]:
self.assertEqual(ref_kml, ptown.kml)
@ -147,8 +143,8 @@ class GeoModelTest(unittest.TestCase):
# Should throw a TypeError when tyring to obtain GML from a
# non-geometry field.
qs = City.objects.all()
self.assertRaises(TypeError, qs.gml, 'name')
ptown1 = City.objects.gml('point', precision=9).get(name='Pueblo')
self.assertRaises(TypeError, qs.gml, field_name='name')
ptown1 = City.objects.gml(field_name='point', precision=9).get(name='Pueblo')
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
if oracle:
@ -172,12 +168,12 @@ class GeoModelTest(unittest.TestCase):
# Asserting the result of the transform operation with the values in
# the pre-transformed points. Oracle does not have the 3084 SRID.
if not oracle:
h = City.objects.transform('point', srid=htown.srid).get(name='Houston')
h = City.objects.transform(htown.srid).get(name='Houston')
self.assertEqual(3084, h.point.srid)
self.assertAlmostEqual(htown.x, h.point.x, prec)
self.assertAlmostEqual(htown.y, h.point.y, prec)
p1 = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
p1 = City.objects.transform(ptown.srid, field_name='point').get(name='Pueblo')
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
for p in [p1, p2]:
self.assertEqual(2774, p.point.srid)
@ -186,7 +182,7 @@ class GeoModelTest(unittest.TestCase):
@no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!)
def test05_extent(self):
"Testing the extent() GeoManager method."
"Testing the `extent` GeoQuerySet method."
if DISABLE: return
# Reference query:
# `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
@ -199,6 +195,17 @@ class GeoModelTest(unittest.TestCase):
for val, exp in zip(extent, expected):
self.assertAlmostEqual(exp, val, 8)
@no_oracle
def test06_make_line(self):
"Testing the `make_line` GeoQuerySet method."
# Ensuring that a `TypeError` is raised on models without PointFields.
self.assertRaises(TypeError, State.objects.make_line)
self.assertRaises(TypeError, Country.objects.make_line)
# Reference query:
# SELECT AsText(ST_MakeLine(geoapp_city.point)) FROM geoapp_city;
self.assertEqual(GEOSGeometry('LINESTRING(-95.363151 29.763374,-96.801611 32.782057,-97.521157 34.464642,174.783117 -41.315268,-104.609252 38.255001,-95.23506 38.971823,-87.650175 41.850385,-123.305196 48.462611)', srid=4326),
City.objects.make_line())
def test09_disjoint(self):
"Testing the `disjoint` lookup type."
if DISABLE: return
@ -317,8 +324,7 @@ class GeoModelTest(unittest.TestCase):
# Saving another commonwealth w/a NULL geometry.
if not oracle:
# TODO: Fix saving w/NULL geometry on Oracle.
nmi = State(name='Northern Mariana Islands', poly=None)
nmi.save()
State(name='Northern Mariana Islands', poly=None).save()
@no_oracle # No specific `left` or `right` operators in Oracle.
def test13_left_right(self):
@ -416,34 +422,36 @@ class GeoModelTest(unittest.TestCase):
c = City()
self.assertEqual(c.point, None)
def test17_union(self):
"Testing the union() GeoManager method."
def test17_unionagg(self):
"Testing the `unionagg` (aggregate union) GeoManager method."
if DISABLE: return
tx = Country.objects.get(name='Texas').mpoly
# Houston, Dallas, San Antonio
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
# Houston, Dallas, San Antonio -- Oracle has different order.
union1 = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
union2 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374,-98.493183 29.424170)')
qs = City.objects.filter(point__within=tx)
self.assertRaises(TypeError, qs.union, 'name')
u1 = qs.union('point')
u2 = qs.union()
self.assertEqual(True, union.equals_exact(u1, 10)) # Going up to 10 digits of precision.
self.assertEqual(True, union.equals_exact(u2, 10))
self.assertRaises(TypeError, qs.unionagg, 'name')
u1 = qs.unionagg(field_name='point')
u2 = qs.unionagg()
tol = 0.00001
if SpatialBackend.oracle:
union = union2
else:
union = union1
self.assertEqual(True, union.equals_exact(u1, tol))
self.assertEqual(True, union.equals_exact(u2, tol))
qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.union('point'))
self.assertEqual(None, qs.unionagg(field_name='point'))
def test18_geometryfield(self):
"Testing GeometryField."
if DISABLE: return
f1 = Feature(name='Point', geom=Point(1, 1))
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
f4 = Feature(name='GeometryCollection',
geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))))
f1.save()
f2.save()
f3.save()
f4.save()
Feature(name='Point', geom=Point(1, 1)).save()
Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))).save()
Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))).save()
Feature(name='GeometryCollection',
geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))).save()
f_1 = Feature.objects.get(name='Point')
self.assertEqual(True, isinstance(f_1.geom, Point))
@ -458,6 +466,82 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
self.assertEqual(f_3.geom, f_4.geom[2])
def test19_centroid(self):
"Testing the `centroid` GeoQuerySet method."
qs = State.objects.exclude(poly__isnull=True).centroid()
if oracle: tol = 0.1
else: tol = 0.000000001
for s in qs:
self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol))
def test20_pointonsurface(self):
"Testing the `point_on_surface` GeoQuerySet method."
# Reference values.
if SpatialBackend.oracle:
# SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) FROM GEOAPP_COUNTRY;
ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326),
'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326),
}
elif SpatialBackend.postgis:
# Using GEOSGeometry to compute the reference point on surface values
# -- since PostGIS also uses GEOS these should be the same.
ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface,
'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface
}
for cntry in Country.objects.point_on_surface():
self.assertEqual(ref[cntry.name], cntry.point_on_surface)
@no_oracle
def test21_scale(self):
"Testing the `scale` GeoQuerySet method."
xfac, yfac = 2, 3
qs = Country.objects.scale(xfac, yfac, model_att='scaled')
for c in qs:
for p1, p2 in zip(c.mpoly, c.scaled):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertEqual(c1[0] * xfac, c2[0])
self.assertEqual(c1[1] * yfac, c2[1])
@no_oracle
def test22_translate(self):
"Testing the `translate` GeoQuerySet method."
xfac, yfac = 5, -23
qs = Country.objects.translate(xfac, yfac, model_att='translated')
for c in qs:
for p1, p2 in zip(c.mpoly, c.translated):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertEqual(c1[0] + xfac, c2[0])
self.assertEqual(c1[1] + yfac, c2[1])
def test23_numgeom(self):
"Testing the `num_geom` GeoQuerySet method."
# Both 'countries' only have two geometries.
for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom)
for c in City.objects.filter(point__isnull=False).num_geom():
# Oracle will return 1 for the number of geometries on non-collections,
# whereas PostGIS will return None.
if postgis: self.assertEqual(None, c.num_geom)
else: self.assertEqual(1, c.num_geom)
def test24_numpoints(self):
"Testing the `num_points` GeoQuerySet method."
for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points)
if postgis:
# Oracle cannot count vertices in Point geometries.
for c in City.objects.num_points(): self.assertEqual(1, c.num_points)
@no_oracle
def test25_geoset(self):
"Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods."
geom = Point(5, 23)
for c in Country.objects.all().intersection(geom).difference(geom).sym_difference(geom).union(geom):
self.assertEqual(c.mpoly.difference(geom), c.difference)
self.assertEqual(c.mpoly.intersection(geom), c.intersection)
self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
self.assertEqual(c.mpoly.union(geom), c.union)
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(GeoModelTest))

View File

@ -169,9 +169,9 @@ class GeoModelTest(unittest.TestCase):
def test07_mysql_limitations(self):
"Testing that union(), kml(), gml() raise exceptions."
self.assertRaises(ImproperlyConfigured, City.objects.union, 'point')
self.assertRaises(ImproperlyConfigured, State.objects.all().kml, 'poly')
self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, 'mpoly')
self.assertRaises(ImproperlyConfigured, City.objects.union, Point(5, 23), field_name='point')
self.assertRaises(ImproperlyConfigured, State.objects.all().kml, field_name='poly')
self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, field_name='mpoly')
def suite():
s = unittest.TestSuite()

View File

@ -19,7 +19,7 @@ class City(models.Model):
name = models.CharField(max_length=25)
population = models.IntegerField()
density = models.DecimalField(max_digits=7, decimal_places=1)
date = models.DateField()
dt = models.DateField()
point = models.PointField()
objects = models.GeoManager()
@ -42,7 +42,7 @@ cofeat_mapping = {'name' : 'Name',
city_mapping = {'name' : 'Name',
'population' : 'Population',
'density' : 'Density',
'date' : 'Created',
'dt' : 'Created',
'point' : 'POINT',
}

View File

@ -69,7 +69,7 @@ class LayerMapTest(unittest.TestCase):
city = City.objects.get(name=feat['Name'].value)
self.assertEqual(feat['Population'].value, city.population)
self.assertEqual(Decimal(str(feat['Density'])), city.density)
self.assertEqual(feat['Created'].value, city.date)
self.assertEqual(feat['Created'].value, city.dt)
# Comparing the geometries.
pnt1, pnt2 = feat.geom, city.point

View File

@ -34,7 +34,7 @@ class RelatedGeoModelTest(unittest.TestCase):
@no_mysql
def test03_transform_related(self):
"Testing the `transform` GeoManager method on related geographic models."
"Testing the `transform` GeoQuerySet method on related geographic models."
# All the transformations are to state plane coordinate systems using
# US Survey Feet (thus a tolerance of 0 implies error w/in 1 survey foot).
if postgis:
@ -47,6 +47,7 @@ class RelatedGeoModelTest(unittest.TestCase):
def check_pnt(ref, pnt):
self.assertAlmostEqual(ref.x, pnt.x, tol)
self.assertAlmostEqual(ref.y, pnt.y, tol)
self.assertEqual(ref.srid, pnt.srid)
# Turning on debug so we can manually verify the number of SQL queries issued.
# DISABLED: the number of queries count testing mechanism is way too brittle.
@ -62,13 +63,35 @@ class RelatedGeoModelTest(unittest.TestCase):
for name, srid, wkt in transformed:
# Doing this implicitly sets `select_related` select the location.
qs = list(City.objects.filter(name=name).transform('location__point', srid))
check_pnt(GEOSGeometry(wkt), qs[0].location.point)
qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point'))
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
#settings.DEBUG= dbg
# Verifying the number of issued SQL queries.
#self.assertEqual(nqueries, len(connection.queries))
@no_mysql
def test04_related_aggregate(self):
"Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models."
if postgis:
# One for all locations, one that excludes Roswell.
all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
txpa_extent = (-97.51611328125, 33.0583305358887,-79.4607315063477, 40.1847610473633)
e1 = City.objects.extent(field_name='location__point')
e2 = City.objects.exclude(name='Roswell').extent(field_name='location__point')
for ref, e in [(all_extent, e1), (txpa_extent, e2)]:
for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val)
# The second union is for a query that has something in the WHERE clause.
ref_u1 = GEOSGeometry('MULTIPOINT(-104.528056 33.387222,-97.516111 33.058333,-79.460734 40.18476)', 4326)
ref_u2 = GEOSGeometry('MULTIPOINT(-97.516111 33.058333,-79.460734 40.18476)', 4326)
u1 = City.objects.unionagg(field_name='location__point')
u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point')
self.assertEqual(ref_u1, u1)
self.assertEqual(ref_u2, u2)
# TODO: Related tests for KML, GML, and distance lookups.
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(RelatedGeoModelTest))